﻿using Dycox.ComponentModel;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Dynamic;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace Dycox.Linq
{
    public class QueryExecutor
    {
        //public event BuildPropertyEventHandler<T> BuildCustomProperty;

        public const char TypeDelimiterReplacement = '_';

        public QueryExecutor(QueryArgs args)
        {
            if (args == null)
                throw new ArgumentNullException("args");

            _arguments = args;
        }

        private QueryArgs _arguments;

        public QueryArgs Arguments
        {
            get { return _arguments; }
            set { _arguments = value; }
        }

        public IQueryable<T> Apply<T>(IQueryable<T> query, BuildCustomQuery<T> buildCustomQuery)
        {
            if (query == null)
                throw new ArgumentNullException("query");

            return BuildQuery(query, buildCustomQuery).Query;
        }

        public QueryResult Execute<T>(IQueryable<T> query)
        {
            return Execute(query, null);
        }

        public QueryResult Execute<T>(IQueryable<T> source, BuildCustomQuery<T> buildCustomQuery)
        {
            QueryResult qr = new QueryResult();
            Populate<T>(qr, source, buildCustomQuery);
            return qr;
        }

        public IQueryable<T> Populate<T>(QueryResult result, IQueryable<T> source)
        {
            return Populate<T>(result, source, null);
        }

        public IQueryable<T> Populate<T>(QueryResult result, IQueryable<T> source, BuildCustomQuery<T> buildCustomQuery)
        {
            int total;
            IQueryable<T> plain;

            BuildExpressionContext<T> context = BuildQuery(source, buildCustomQuery);
            SortOrderSettings sos = _arguments.GetSort();
            plain = source = context.Query;

            if (sos != null)
                source = ApplySortOrder(source, sos);

            if (_arguments.EnablePaging)
            {
                PagingInfo paging = new PagingInfo(_arguments.PageIndex, _arguments.PageSize);
                source = paging.Page(source);
                total = paging.Total;
            }
            else
                total = 0;


            Array array;

            if (_arguments.View != null)
            {
                IQueryable q = Transform(context);
                System.Collections.ArrayList list = new System.Collections.ArrayList();
                var e = q.GetEnumerator();
                while (e.MoveNext())
                    list.Add(e.Current);

                array = list.ToArray(q.ElementType);
            }
            else
                array = source.ToArray();

            result.Initialize(array, _arguments, total);

            return plain;
        }

        public IQueryable<T> ApplySortOrder<T>(IQueryable<T> source)
        {
            if (source == null)
                throw new ArgumentNullException("source");

            SortOrderSettings sos = _arguments.GetSort();

            if (sos != null)
            {
                source = ApplySortOrder(source, sos);
            }

            return source;
        }

        public static IQueryable<T> ApplySortOrder<T>(IQueryable<T> source, SortOrderSettings sos)
        {
            Type elementType = typeof(T);
            return (IQueryable<T>)ApplySortOrderCore(source, sos);
        }

        private static IQueryable ApplySortOrderCore(IQueryable source, SortOrderSettings sos)
        {
            Type elementType = source.ElementType;
            ParameterExpression par = Expression.Parameter(elementType);
            PropertyInfo property;
            string methodName = "OrderBy";

            foreach (SortOrderEntry entry in sos.GetEntries())
            {
                property = elementType.GetProperty(entry.Key);
                if (property == null)
                {
                    //property = ResolveKey(entry.Key);
                    if (property == null)
                        throw new InvalidOperationException("Property " + entry.Key + " not found on " + elementType.FullName);
                }

                var expr = Expression.Lambda(Expression.Property(par, property), par);

                if (entry.Order == SortOrder.Descending)
                    methodName += "Descending";

                source = source.Provider.CreateQuery(Expression.Call(null,
                    TypicalLinqMethods.GetMethod("Queryable" + methodName).MakeGenericMethod(elementType, property.PropertyType),
                    source.Expression, Expression.Quote(expr)));

                methodName = "ThenBy";
            }
            return source;
        }


        private BuildExpressionContext<T> BuildQuery<T>(IQueryable<T> source, BuildCustomQuery<T> buildCustomQuery)
        {
            QueryArgsSurrogate surrogate = new QueryArgsSurrogate(this._arguments);
            BuildExpressionContext<T> context = new BuildExpressionContext<T>(source, buildCustomQuery);

            foreach (PropertyState state in PrepareProperties(surrogate))
            {
                ExpressionFragment fragment = context.StartFragment(state.Property, state.Value);
                BuildFragment(fragment, context);
                context.EndFragment();
            }

            return context;
        }

        private List<PropertyState> PrepareProperties(QueryArgsSurrogate surrogate)
        {
            List<PropertyState> list, overrides;
            PropertyDescriptorCollection coll = TypeDescriptor.GetProperties(surrogate);

            list = new List<PropertyState>(coll.Count);
            overrides = null;

            foreach (QueryProperty qp in coll)
            {
                object value;

                if (ShouldBuildProperty(qp, surrogate, out value))
                {
                    PropertyState state = new PropertyState();
                    state.Property = qp;
                    state.Value = value;

                    if (state.Overrides.Length > 0)
                    {
                        if (overrides == null)
                            overrides = new List<PropertyState>(4);
                        overrides.Add(state);
                    }
                    else
                        list.Add(state);
                }
            }

            if (overrides != null)
            {
                foreach (PropertyState over in overrides)
                {
                    foreach (var attr in over.Overrides)
                    {
                        if (attr.ShouldOverride(surrogate.QueryArgs))
                        {
                            string name = attr.PropertyName;

                            if (name == OverrideAttribute.WildCard)
                            {
                                list.Clear();
                            }
                            else
                            {
                                int index = list.FindIndex(o => o.Property.Name == name);
                                if (index >= 0)
                                    list.RemoveAt(index);
                            }
                        }
                    }

                }

                list.AddRange(overrides);
            }

            return list;
        }

        private IQueryable Transform<T>(BuildExpressionContext<T> context)
        {
            ModelView view = _arguments.View;
            Type type;
            List<ViewFieldProperty> list = new List<ViewFieldProperty>();

            foreach (ViewField field in view.GetFields(true))
            {
                QueryOperatorAttribute qoa = new QueryOperatorAttribute(field.PropertyName);
                MemberExpression member = GetProperty(qoa, null, context);
                if (member != null)
                {
                    ViewFieldProperty dp = new ViewFieldProperty(
                        field.Name.Replace(Type.Delimiter, TypeDelimiterReplacement), member);

                    list.Add(dp);
                }
            }

            type = System.Linq.Dynamic.DynamicExpression.CreateClass(list);

            var lambda = Expression.Lambda(Expression.MemberInit(
                Expression.New(type),
                list.Select(o => Expression.Bind(type.GetProperty(o.Name), o.Expression)).ToArray()),
                context.Parameter);
            
            return (IQueryable)TypicalLinqMethods.QueryableSelect
                .MakeGenericMethod(typeof(T), type)
                .Invoke(null, new object[] { context.Query, lambda });
        }

        private bool ShouldBuildProperty(QueryProperty property, QueryArgsSurrogate instance, out object value)
        {
            value = property.GetValue(instance);

            if (value == null) return false;

            if (value.GetType() == typeof(string) && ((string)value).Length == 0 && property.IgnoreEmptyString)
                return false;

            if (property.IgnoreDefaultValue)
            {
                DefaultValueAttribute dva = (DefaultValueAttribute)property.Attributes[typeof(DefaultValueAttribute)];

                if (dva != null && object.Equals(dva.Value, value))
                {
                    return false;
                }
            }

            return true;
        }

        private MemberExpression GetProperty<T>(QueryOperatorAttribute qoa, ExpressionFragment fragment, BuildExpressionContext<T> context)
        {
            string[] path;
            Expression owner = context.Parameter;
            Type type = context.Parameter.Type;

            if (string.IsNullOrEmpty(qoa.TargetPropertyName))
            {
                //return Expression.Property(context.Parameter, type, fragment.ArgumentProperty.Name);
                path = new string[] { string.Empty };
            }
            else
                path = qoa.TargetPropertyName.Split(Type.Delimiter);

            if (path[0].Length == 0) path[0] = fragment.ArgumentProperty.Name;

            foreach (string name in path)
            {
                PropertyInfo info = type.GetProperty(name);
                if (info == null)
                {
                    if (qoa.Operator == QueryOperator.Custom)
                        return null;
                    else
                        throw new InvalidOperationException("Property " + name + " not found on " + type.FullName);
                }
                MemberExpression me = Expression.Property(owner, info);
                owner = me;
                type = info.PropertyType;
            }

            return (MemberExpression)owner;
        }

        private void BuildFragment<T>(ExpressionFragment fragment, BuildExpressionContext<T> context)
        {
            foreach (QueryOperatorAttribute qoa in fragment.Operators)
            {
                MemberExpression member;
                member = GetProperty(qoa, fragment, context);

                if (qoa.Operator == QueryOperator.Custom)
                {
                    var callback = context.Callback;
                    if (callback == null)
                        throw new InvalidOperationException("BuildProperty event not handled.");

                    PropertyInfo target;

                    if (member != null) target = (PropertyInfo)member.Member;
                    else target = null;

                    context.Query = callback(context.Query, fragment.ArgumentProperty, fragment.ArgumentValue, target);
                }
                else
                {
                    var expr = BuildFragment(qoa, fragment, member, context);
                    if (expr != null)
                        fragment.Or(expr);
                }
            }
        }



        private Expression BuildFragment<T>(QueryOperatorAttribute qoa, ExpressionFragment fragment, MemberExpression member, BuildExpressionContext<T> context)
        {
            object value = fragment.ArgumentValue;
            Type type = value.GetType();
            QueryOperator op = qoa.Operator;

            if (type.IsArray)
            {
                Array array = (Array)value;
                int length = array.Length;
                if (length > 0)
                {
                    if (length > 1)
                    {
                        MethodInfo contains = TypicalLinqMethods.EnumerableContains.MakeGenericMethod(type.GetElementType());
                        return Expression.Call(null, contains, Expression.Constant(array), member);
                    }
                    else
                    {
                        value = array.GetValue(0);
                        op = QueryOperator.Equals;
                    }
                }
                else
                    return null;
            }

            if (fragment.ArgumentProperty.StandardItemsSupport != StandardItemsSupport.None)
            {
                if (value is IListItem)
                    value = ((IListItem)value).Value;
            }

            if (op == QueryOperator.Default)
            {
                if (type == typeof(string))
                    op = QueryOperator.Contains;
                else if (value is IRange)
                    op = QueryOperator.Between;
                else
                    op = QueryOperator.Equals;
            }

            if (((int)op) > 0 && (int)op < 10)
            {
                Expression left, right;
                Type ptype = ((PropertyInfo)member.Member).PropertyType;

                if (ptype.IsEnum)
                {
                    // Linq to sql does NOT support <, >, <=, >= operators of Enum type.
                    // Must convert them to System.Int32 before creating binary expression.
                    ptype = Enum.GetUnderlyingType(ptype);

                    left = Expression.Convert(member, ptype);
                    right = Expression.Constant(Convert.ChangeType(value, ptype), ptype);
                }
                else
                {
                    left = member;
                    right = Expression.Constant(value, ptype);
                }

                switch (op)
                {
                    case QueryOperator.Equals:
                        return Expression.Equal(left, right);
                    case QueryOperator.NotEquals:
                        return Expression.NotEqual(left, right);
                    case QueryOperator.LessThan:
                        return Expression.LessThan(left, right);
                    case QueryOperator.LessThanOrEquals:
                        return Expression.LessThanOrEqual(left, right);
                    case QueryOperator.GreaterThan:
                        return Expression.GreaterThan(left, right);
                    case QueryOperator.GreaterThanOrEquals:
                        return Expression.GreaterThanOrEqual(left, right);
                    default:
                        return null;
                }
            }
            else
            {
                switch (op)
                {
                    case QueryOperator.Between:
                        return BuildBetweenExpression((IRange)value, member);
                    case QueryOperator.NullTest:
                        return BuildNullTestExpression(value, ((PropertyInfo)member.Member).PropertyType, member);
                    case QueryOperator.Contains:
                    case QueryOperator.StartsWith:
                    case QueryOperator.EndsWith:
                        return BuildStringExpression(value, op, member);
                    default:
                        return null;
                }
            }
        }

        private static Expression BuildBetweenExpression(IRange range, MemberExpression propexpr)
        {

            if (range != null)
            {
                Expression expr;
                
                Type t = ((PropertyInfo)propexpr.Member).PropertyType;

                expr = BuildBetweenExpression(propexpr, range, t);

                return expr;
            }
            else
                throw new InvalidOperationException("");
        }

        internal static Expression BuildBetweenExpression(Expression body, IRange range, Type type)
        {
            Expression expr = null;

            if (range.IsLowerSpecified)
            {
                object l = range.LowerBound;
                expr = Expression.GreaterThanOrEqual(body, Expression.Constant(l, type));
            }

            if (range.IsUpperSpecified)
            {
                object u = range.UpperBound;
                Expression tmp = Expression.LessThanOrEqual(body, Expression.Constant(u, type));
                if (expr == null) expr = tmp;
                else expr = Expression.And(expr, tmp);
            }

            return expr;
        }


        private Expression BuildStringExpression(object value, QueryOperator op, MemberExpression parameterExpression)
        {
            MethodInfo method = TypicalLinqMethods.GetMethod("String" + op.ToString());
            string s = value.ToString();
            return Expression.Call(parameterExpression, method, Expression.Constant(s));
        }

        private Expression BuildNullTestExpression(object isNull, Type propertyType, MemberExpression paramexpr)
        {
            if (isNull.GetType() == typeof(bool))
            {
                if ((bool)isNull)
                    return Expression.Equal(paramexpr, Expression.Constant(null, propertyType), false, null);
                else
                    return Expression.NotEqual(paramexpr, Expression.Constant(null, propertyType), false, null);
            }
            else
                throw new InvalidOperationException("For NullTest operator, the property type of QueryArgs must set to System.Boolean or System.Nullable<System.Boolean>.");
        }

        //private IQueryable<T> BuildQueryProperty(QueryProperty property, object value, IQueryable<T> query)
        //{
        //    var array = property.Attributes.OfType<QueryOperatorAttribute>().ToArray();
        //    Expression expr;

        //    if (array.Length == 0)
        //    {
        //        array = new QueryOperatorAttribute[] { new QueryOperatorAttribute(QueryOperator.Default, property.Name) };
        //    }

        //    foreach (var qoa in array)
        //    {
        //        expr = BuildExpression(qoa, value, property);
        //    }

        //}

        class BuildExpressionContext<T>
        {
            public BuildExpressionContext(IQueryable<T> query, BuildCustomQuery<T> callback)
            {
                _parameter = Expression.Parameter(typeof(T), "obj");
                _source = _query = query;
                _callback = callback;
            }

            private IQueryable<T> _query;

            public IQueryable<T> Query
            {
                get { return _query; }
                set { _query = value; }
            }

            private IQueryable<T> _source;

            public IQueryable<T> Source
            {
                get { return _source; }
            }

            private ParameterExpression _parameter;

            public ParameterExpression Parameter
            {
                get { return _parameter; }
            }

            private ExpressionFragment _fragment;

            public ExpressionFragment StartFragment(QueryProperty property, object value)
            {
                ExpressionFragment fragment = new ExpressionFragment(property, value);
                _fragment = fragment;
                return fragment;
            }

            public ExpressionFragment CurrentFragment
            {
                get
                {
                    return _fragment;
                }
            }

            public void EndFragment()
            {
                Expression expr = _fragment.Expression;
                if (expr != null)
                {
                    _query = _query.Where(Expression.Lambda<Func<T, bool>>(expr, _parameter));
                }
            }

            private BuildCustomQuery<T> _callback;

            public BuildCustomQuery<T> Callback
            {
                get { return _callback; }
            }

        }

        class ExpressionFragment
        {
            public ExpressionFragment(QueryProperty property, object value)
            {
                _argumentProperty = property;
                _argumentValue = value;
                
                var array = property.Attributes.OfType<QueryOperatorAttribute>().ToArray();

                if (array.Length == 0)
                {
                    array = new QueryOperatorAttribute[] { new QueryOperatorAttribute(QueryOperator.Default, property.Name) };
                }

                _operators = array;
            }

            private QueryOperatorAttribute[] _operators;

            public QueryOperatorAttribute[] Operators
            {
                get { return _operators; }
            }

            private QueryProperty _argumentProperty;

            public QueryProperty ArgumentProperty
            {
                get { return _argumentProperty; }
            }

            private object _argumentValue;

            public object ArgumentValue
            {
                get { return _argumentValue; }
            }

            private Expression _expression;

            public Expression Expression
            {
                get { return _expression; }
                //set { _expression = value; }
            }

            public void And(Expression expr)
            {
                if (_expression == null) _expression = expr;
                else
                {
                    _expression = Expression.And(_expression, expr);
                }

            }

            public void Or(Expression expr)
            {
                if (_expression == null) _expression = expr;
                else
                {
                    _expression = Expression.Or(_expression, expr);
                }
            }
        }

        class ViewFieldProperty : DynamicProperty
        {
            public ViewFieldProperty(string name, MemberExpression expr)
                : base(name, ((PropertyInfo)expr.Member).PropertyType)
            {
                _expression = expr;
            }

            private MemberExpression _expression;

            public MemberExpression Expression
            {
                get { return _expression; }
            }

        }

        class PropertyState
        {
            private QueryProperty _property;

            public QueryProperty Property
            {
                get { return _property; }
                set
                {
                    _property = value;
                    _overrides = null;
                }
            }

            public object Value;

            OverrideAttribute[] _overrides;

            public OverrideAttribute[] Overrides
            {
                get
                {
                    if (_overrides == null && _property != null)
                    {
                        _overrides = _property.Attributes.OfType<OverrideAttribute>().ToArray();
                    }

                    return _overrides;
                }
            }

        }
    }

    public delegate IQueryable<T> BuildCustomQuery<T>(IQueryable<T> query, QueryProperty argumentProperty, object argumentValue, PropertyInfo targetProperty);

}
